PyCallGraph一览

premature optimization is the root of all evil

by Donald Knuth

最近在尝试重构EE-Book,每过一段时间看以前写的代码是一件很有意思的事情,这是跟愚蠢的自己沟通的过程,显然这也是进步的过程。

扯远了,这一篇是要总结+介绍一款分析源代码的工具。在分析EE-Book代码的过程中,我发现了两款工具,一款是Pyreverse,另一款是PyCallGraph。Pyreverse可以生成UML类图,它已集成到pylint中,它的作用是分析项目代码生成像这样的UML图:

pyreverse

来自pylint

显然,这个工具能够帮助我们很快地理解一个Python项目的结构,有了这样的UML类图再去看源代码就会清晰很多。但目前我想要分析的是自己的代码,虽然Pyreverse很屌,但用不上啊,所以,这个工具暂且放一放,以后需要再学习一个,多说一句,如果想画UML也可以用plantuml

今天的主角是强大的PyCallGraph,一句话简介:PyCallGraph可以动态生成python程序的调用图。它能告诉你各函数调用次数、调用时间。先拿官方的说明举例吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import time

from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput


class Banana:

def __init__(self):
pass

def eat(self):
self.secret_function()
self.chew()
self.swallow()

def secret_function(self):
time.sleep(0.2)

def chew(self):
pass

def swallow(self):
pass


graphviz = GraphvizOutput(output_file='filter_none.png')

with PyCallGraph(output=graphviz):
banana = Banana()
banana.eat()

现在有一个Banana类,我想要生成eat方法的调用关系,看看时间都用在哪里。只需要通过GraphvizOutput指定output文件,进行初始化,通过Python的with关键字,将运行的代码放入with代码块即可生成调用关系图:
filter_none

可以看到,secret_funcition这部分是红色的,运行时间是最长的,如果是优化实际项目,我们就应该从这入手了。

是不是非常简单,非常优雅?就这么一张图,告诉了我们程序的运行路径,经过的每个函数、每个模块、运行的时间、调用的次数,简简单单就了解了程序的整个调用过程,其实坑还是有的,比如我生成EE-Book爬取简书某博主的文章的调用关系图:

图片太大,请点击查看

完全没法分析是不是,对的,我们和PyCallGraph的作者想到一起去了,稍微大点的项目,有的函数是不需要分析的,不然生成的图太复杂了,得有个过滤机制才行,暂时不分析一些函数,对调用的深度也得加个限制,写一个过滤函数的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
config = Config(max_depth=9)
config.trace_filter = GlobbingFilter(exclude=[
'bs4.*',
'BeautifulSoup.*',
# '.epub.*',
# '__main__',
'pycallgraph.*',
'*.closeEvent',
'*.<module>',
'src.tools.match.*',
'*.EEBook.__init__',
'src.main.init_database',
'src.tools.config_load',
'src.utils.log.*',
'src.tools.path.*',
'src.tools.db.*',
'src.book.*',
'src.container.*',
'src.url_parser.*',
'src.tools.config.*',
'src.lib.epub.*',
'src.tools.html_creator.*',
'*.JianshuAuthorWorker.__init__',
'*.start_create_work_list',
'*.JianshuAuthorWorker.save',
'*.JianshuAuthorWorker.add_property',
'*.JianshuAuthorWorker.clear_index',
'*.JianshuAuthorWorker.create_save_config',
'*.JianshuAuthorWorker.clear_work_set',
'*.print_in_single_line',
'*.http.set_cookie',
'*.html5lib.constants.*',
])

再看看生成的图:

filter_exclude_jianshu

这下就要清晰多了吧,接下来就可以继续修改过滤机制,庖丁解牛般查找问题所在。

利用PyCallGraph我们能够很快找到程序需要优化的地方,接下来选择合适的方案即可。比如如果时间大多花在I/O上,应该试试Python的多线程模块,如果解析字符串花的时间比较多,可能我们得放弃bs4库,改成正则表达式?再或者,计算密集型,尝试用C语言扩展重写?其实说到底还是要有一针见血找到问题所在的能力(体现思考深度),找到问题所在再用对应方案尝试解决(体现思考广度)。要优化代码,有PyCallGraph这样的可视化工具帮助我们剖析代码,难道还不够么?

还不够么

不够么

够么

欢迎用更好的工具打我脸啊~